{
 "cells": [
  {
   "cell_type": "code",
   "execution_count": 1,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2020-11-03T19:50:50.150235Z",
     "start_time": "2020-11-03T19:50:48.888079Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "%pylab is deprecated, use %matplotlib inline and import the required libraries.\n",
      "Populating the interactive namespace from numpy and matplotlib\n"
     ]
    }
   ],
   "source": [
    "%pylab inline"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Notebook magic"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2022-01-07T19:16:29.118001Z",
     "start_time": "2022-01-07T19:16:29.114692Z"
    }
   },
   "outputs": [],
   "source": [
    "from IPython.core.magic import Magics, magics_class, line_cell_magic\n",
    "from IPython.core.magic import cell_magic, register_cell_magic, register_line_magic\n",
    "from IPython.core.magic_arguments import argument, magic_arguments, parse_argstring\n",
    "import subprocess\n",
    "import os"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2022-01-07T19:16:37.453883Z",
     "start_time": "2022-01-07T19:16:37.422478Z"
    }
   },
   "outputs": [],
   "source": [
    "@magics_class\n",
    "class PyboardMagic(Magics):\n",
    "    @cell_magic\n",
    "    @magic_arguments()\n",
    "    @argument('-skip')\n",
    "    @argument('-unix')\n",
    "    @argument('-pyboard')\n",
    "    @argument('-file')\n",
    "    @argument('-data')\n",
    "    @argument('-time')\n",
    "    @argument('-memory')\n",
    "    def micropython(self, line='', cell=None):\n",
    "        args = parse_argstring(self.micropython, line)\n",
    "        if args.skip: # doesn't care about the cell's content\n",
    "            print('skipped execution')\n",
    "            return None # do not parse the rest\n",
    "        if args.unix: # tests the code on the unix port. Note that this works on unix only\n",
    "            with open('/dev/shm/micropython.py', 'w') as fout:\n",
    "                fout.write(cell)\n",
    "            proc = subprocess.Popen([\"../micropython/ports/unix/build-2/micropython-2\", \"/dev/shm/micropython.py\"], \n",
    "                                    stdout=subprocess.PIPE, stderr=subprocess.PIPE)\n",
    "            print(proc.stdout.read().decode(\"utf-8\"))\n",
    "            print(proc.stderr.read().decode(\"utf-8\"))\n",
    "            return None\n",
    "        if args.file: # can be used to copy the cell content onto the pyboard's flash\n",
    "            spaces = \"    \"\n",
    "            try:\n",
    "                with open(args.file, 'w') as fout:\n",
    "                    fout.write(cell.replace('\\t', spaces))\n",
    "                    printf('written cell to {}'.format(args.file))\n",
    "            except:\n",
    "                print('Failed to write to disc!')\n",
    "            return None # do not parse the rest\n",
    "        if args.data: # can be used to load data from the pyboard directly into kernel space\n",
    "            message = pyb.exec(cell)\n",
    "            if len(message) == 0:\n",
    "                print('pyboard >>>')\n",
    "            else:\n",
    "                print(message.decode('utf-8'))\n",
    "                # register new variable in user namespace\n",
    "                self.shell.user_ns[args.data] = string_to_matrix(message.decode(\"utf-8\"))\n",
    "        \n",
    "        if args.time: # measures the time of executions\n",
    "            pyb.exec('import utime')\n",
    "            message = pyb.exec('t = utime.ticks_us()\\n' + cell + '\\ndelta = utime.ticks_diff(utime.ticks_us(), t)' + \n",
    "                               \"\\nprint('execution time: {:d} us'.format(delta))\")\n",
    "            print(message.decode('utf-8'))\n",
    "        \n",
    "        if args.memory: # prints out memory information \n",
    "            message = pyb.exec('from micropython import mem_info\\nprint(mem_info())\\n')\n",
    "            print(\"memory before execution:\\n========================\\n\", message.decode('utf-8'))\n",
    "            message = pyb.exec(cell)\n",
    "            print(\">>> \", message.decode('utf-8'))\n",
    "            message = pyb.exec('print(mem_info())')\n",
    "            print(\"memory after execution:\\n========================\\n\", message.decode('utf-8'))\n",
    "\n",
    "        if args.pyboard:\n",
    "            message = pyb.exec(cell)\n",
    "            print(message.decode('utf-8'))\n",
    "\n",
    "ip = get_ipython()\n",
    "ip.register_magics(PyboardMagic)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## pyboard"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 57,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2020-05-07T07:35:35.126401Z",
     "start_time": "2020-05-07T07:35:35.105824Z"
    }
   },
   "outputs": [],
   "source": [
    "import pyboard\n",
    "pyb = pyboard.Pyboard('/dev/ttyACM0')\n",
    "pyb.enter_raw_repl()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2020-05-19T19:11:18.145548Z",
     "start_time": "2020-05-19T19:11:18.137468Z"
    }
   },
   "outputs": [],
   "source": [
    "pyb.exit_raw_repl()\n",
    "pyb.close()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 58,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2020-05-07T07:35:38.725924Z",
     "start_time": "2020-05-07T07:35:38.645488Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "\n"
     ]
    }
   ],
   "source": [
    "%%micropython -pyboard 1\n",
    "\n",
    "import utime\n",
    "from ulab import numpy as np\n",
    "\n",
    "def timeit(n=1000):\n",
    "    def wrapper(f, *args, **kwargs):\n",
    "        func_name = str(f).split(' ')[1]\n",
    "        def new_func(*args, **kwargs):\n",
    "            run_times = np.zeros(n, dtype=np.uint16)\n",
    "            for i in range(n):\n",
    "                t = utime.ticks_us()\n",
    "                result = f(*args, **kwargs)\n",
    "                run_times[i] = utime.ticks_diff(utime.ticks_us(), t)\n",
    "            print('{}() execution times based on {} cycles'.format(func_name, n, (delta2-delta1)/n))\n",
    "            print('\\tbest: %d us'%np.min(run_times))\n",
    "            print('\\tworst: %d us'%np.max(run_times))\n",
    "            print('\\taverage: %d us'%np.mean(run_times))\n",
    "            print('\\tdeviation: +/-%.3f us'%np.std(run_times))            \n",
    "            return result\n",
    "        return new_func\n",
    "    return wrapper\n",
    "\n",
    "def timeit(f, *args, **kwargs):\n",
    "    func_name = str(f).split(' ')[1]\n",
    "    def new_func(*args, **kwargs):\n",
    "        t = utime.ticks_us()\n",
    "        result = f(*args, **kwargs)\n",
    "        print('execution time: ', utime.ticks_diff(utime.ticks_us(), t), ' us')\n",
    "        return result\n",
    "    return new_func"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "__END_OF_DEFS__"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Numerical\n",
    "\n",
    "Function in this section can be used for calculating statistical properties, or manipulating the arrangement of array elements."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## min, argmin, max, argmax\n",
    "\n",
    "`numpy`: https://docs.scipy.org/doc/numpy/reference/generated/numpy.min.html\n",
    "\n",
    "`numpy`: https://docs.scipy.org/doc/numpy/reference/generated/numpy.argmax.html\n",
    "\n",
    "`numpy`: https://docs.scipy.org/doc/numpy/reference/generated/numpy.max.html\n",
    "\n",
    "`numpy`: https://docs.scipy.org/doc/numpy/reference/generated/numpy.argmax.html\n",
    "\n",
    "**WARNING:** Difference to `numpy`: the `out` keyword argument is not implemented.\n",
    "\n",
    "These functions follow the same pattern, and work with generic iterables, and `ndarray`s. `min`, and `max` return the minimum or maximum of a sequence. If the input array is two-dimensional, the `axis` keyword argument can be supplied, in which case the minimum/maximum along the given axis will be returned. If `axis=None` (this is also the default value), the minimum/maximum of the flattened array will be determined. The functions also accept the `keepdims=True` or `keepdims=False` keyword argument. The latter case is the default, while the former keeps the dimensions (the number of axes) of the supplied array. \n",
    "\n",
    "`argmin/argmax` return the position (index) of the minimum/maximum in the sequence."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2019-10-18T13:08:28.113525Z",
     "start_time": "2019-10-18T13:08:28.093518Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "a: array([1.0, 2.0, 0.0, 1.0, 10.0], dtype=float64)\n",
      "min of a: 0.0\n",
      "argmin of a: 2\n",
      "\n",
      "b:\n",
      " array([[1.0, 2.0, 0.0],\n",
      "       [1.0, 10.0, -1.0]], dtype=float64)\n",
      "min of b (flattened): -1.0\n",
      "min of b (axis=0): array([1.0, 2.0, -1.0], dtype=float64)\n",
      "min of b (axis=1): array([0.0, -1.0], dtype=float64)\n",
      "\n",
      "\n"
     ]
    }
   ],
   "source": [
    "%%micropython -unix 1\n",
    "\n",
    "from ulab import numpy as np\n",
    "\n",
    "a = np.array([1, 2, 0, 1, 10])\n",
    "print('a:', a)\n",
    "print('min of a:', np.min(a))\n",
    "print('argmin of a:', np.argmin(a))\n",
    "\n",
    "b = np.array([[1, 2, 0], [1, 10, -1]])\n",
    "print('\\nb:\\n', b)\n",
    "print('min of b (flattened):', np.min(b))\n",
    "print('min of b (axis=0):', np.min(b, axis=0))\n",
    "print('min of b (axis=1):', np.min(b, axis=1))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "a: array([[0.0, 1.0, 2.0, 3.0],\n",
      "       [4.0, 5.0, 6.0, 7.0],\n",
      "       [8.0, 9.0, 10.0, 11.0]], dtype=float64)\n",
      "\n",
      "min of a (axis=1):\n",
      " array([[0.0],\n",
      "       [4.0],\n",
      "       [8.0]], dtype=float64)\n",
      "\n",
      "min of a (axis=0):\n",
      " array([[0.0, 1.0, 2.0, 3.0]], dtype=float64)\n",
      "\n",
      "\n"
     ]
    }
   ],
   "source": [
    "%%micropython -unix 1\n",
    "\n",
    "from ulab import numpy as np\n",
    "\n",
    "a = np.array(range(12)).reshape((3, 4))\n",
    "\n",
    "print('a:', a)\n",
    "print('\\nmin of a (axis=1):\\n', np.min(a, axis=1, keepdims=True))\n",
    "print('\\nmin of a (axis=0):\\n', np.min(a, axis=0, keepdims=True))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## sum, std, mean\n",
    "\n",
    "`numpy`: https://docs.scipy.org/doc/numpy/reference/generated/numpy.sum.html\n",
    "\n",
    "`numpy`: https://docs.scipy.org/doc/numpy/reference/generated/numpy.std.html\n",
    "\n",
    "`numpy`: https://docs.scipy.org/doc/numpy/reference/generated/numpy.mean.html\n",
    "\n",
    "These three functions follow the same pattern: if the `axis` keyword is not specified, they assume the default value of `None`, and return the result of the computation for the flattened array. Otherwise, the calculation is along the given axis. \n",
    "\n",
    "If the `axis` keyword argument is a number (this can also be negative to signify counting from the rightmost axis) the functions contract the arrays, i.e., the results will have one axis fewer than the input array. The only exception to this rule is when the `keepdims` keyword argument is supplied with a value `True`, in which case, the results will have the same number of axis as the input, but the axis specified in `axis` will have a length of 1. This is useful in cases, when the output is to be broadcast with the input in subsequent computations."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2019-10-20T06:51:58.845076Z",
     "start_time": "2019-10-20T06:51:58.798730Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "a: \n",
      " array([[1.0, 2.0, 3.0],\n",
      "       [4.0, 5.0, 6.0],\n",
      "       [7.0, 8.0, 9.0]], dtype=float64)\n",
      "sum, flat array:  45.0\n",
      "mean, horizontal:  array([2.0, 5.0, 8.0], dtype=float64)\n",
      "std, vertical:  array([2.449489742783178, 2.449489742783178, 2.449489742783178], dtype=float64)\n",
      "\n",
      "\n"
     ]
    }
   ],
   "source": [
    "%%micropython -unix 1\n",
    "\n",
    "from ulab import numpy as np\n",
    "\n",
    "a = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])\n",
    "print('a: \\n', a)\n",
    "\n",
    "print('sum, flat array: ', np.sum(a))\n",
    "\n",
    "print('mean, horizontal: ', np.mean(a, axis=1))\n",
    "\n",
    "print('std, vertical: ', np.std(a, axis=0))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 52,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "a: \n",
      " array([[1.0, 2.0, 3.0],\n",
      "       [4.0, 5.0, 6.0],\n",
      "       [7.0, 8.0, 9.0]], dtype=float64)\n",
      "\n",
      "std, along 0th axis:\n",
      " array([2.449489742783178, 2.449489742783178, 2.449489742783178], dtype=float64)\n",
      "\n",
      "a: \n",
      " array([[1.0, 2.0, 3.0],\n",
      "       [4.0, 5.0, 6.0],\n",
      "       [7.0, 8.0, 9.0]], dtype=float64)\n",
      "\n",
      "std, along 1st axis, keeping dimensions:\n",
      " array([[0.8164965809277261],\n",
      "       [0.8164965809277261],\n",
      "       [0.8164965809277261]], dtype=float64)\n",
      "\n",
      "\n"
     ]
    }
   ],
   "source": [
    "%%micropython -unix 1\n",
    "\n",
    "from ulab import numpy as np\n",
    "\n",
    "a = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])\n",
    "print('a: \\n', a)\n",
    "print('\\nstd, along 0th axis:\\n', np.std(a, axis=0))\n",
    "\n",
    "print('\\na: \\n', a)\n",
    "print('\\nstd, along 1st axis, keeping dimensions:\\n', np.std(a, axis=1, keepdims=True))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## roll\n",
    "\n",
    "`numpy`: https://docs.scipy.org/doc/numpy/reference/generated/numpy.roll.html\n",
    "\n",
    "The roll function shifts the content of a vector by the positions given as the second argument. If the `axis` keyword is supplied, the shift is applied to the given axis."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 229,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2019-10-11T19:39:47.459395Z",
     "start_time": "2019-10-11T19:39:47.443691Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "a:\t\t\t array([1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0], dtype=float)\n",
      "a rolled to the left:\t array([3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 1.0, 2.0], dtype=float)\n",
      "a rolled to the right:\t array([1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0], dtype=float)\n",
      "\n",
      "\n"
     ]
    }
   ],
   "source": [
    "%%micropython -unix 1\n",
    "\n",
    "from ulab import numpy as np\n",
    "\n",
    "a = np.array([1, 2, 3, 4, 5, 6, 7, 8])\n",
    "print(\"a:\\t\\t\\t\", a)\n",
    "\n",
    "np.roll(a, 2)\n",
    "print(\"a rolled to the left:\\t\", a)\n",
    "\n",
    "# this should be the original vector\n",
    "numerical.roll(a, -2)\n",
    "print(\"a rolled to the right:\\t\", a)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Rolling works with matrices, too. If the `axis` keyword is 0, the matrix is rolled along its vertical axis, otherwise, horizontally. \n",
    "\n",
    "Horizontal rolls are faster, because they require fewer steps, and larger memory chunks are copied, however, they also require more RAM: basically the whole row must be stored internally. Most expensive are the `None` keyword values, because with `axis = None`, the array is flattened first, hence the row's length is the size of the whole matrix.\n",
    "\n",
    "Vertical rolls require two internal copies of single columns. "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 268,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2019-10-15T17:46:20.051069Z",
     "start_time": "2019-10-15T17:46:20.033205Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "a:\n",
      " array([[1.0, 2.0, 3.0, 4.0],\n",
      "\t [5.0, 6.0, 7.0, 8.0]], dtype=float)\n",
      "\n",
      "a rolled to the left:\n",
      " array([[3.0, 4.0, 5.0, 6.0],\n",
      "\t [7.0, 8.0, 1.0, 2.0]], dtype=float)\n",
      "\n",
      "a rolled up:\n",
      " array([[6.0, 3.0, 4.0, 5.0],\n",
      "\t [2.0, 7.0, 8.0, 1.0]], dtype=float)\n",
      "\n",
      "a rolled with None:\n",
      " array([[3.0, 4.0, 5.0, 2.0],\n",
      "\t [7.0, 8.0, 1.0, 6.0]], dtype=float)\n",
      "\n",
      "\n"
     ]
    }
   ],
   "source": [
    "%%micropython -unix 1\n",
    "\n",
    "from ulab import numpy as np\n",
    "\n",
    "a = np.array([[1, 2, 3, 4], [5, 6, 7, 8]])\n",
    "print(\"a:\\n\", a)\n",
    "\n",
    "np.roll(a, 2)\n",
    "print(\"\\na rolled to the left:\\n\", a)\n",
    "\n",
    "np.roll(a, -1, axis=1)\n",
    "print(\"\\na rolled up:\\n\", a)\n",
    "\n",
    "np.roll(a, 1, axis=None)\n",
    "print(\"\\na rolled with None:\\n\", a)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Simple running weighted average\n",
    "\n",
    "As a demonstration of the conciseness of `ulab/numpy` operations, we will calculate an exponentially weighted running average of a measurement vector in just a couple of lines. I chose this particular example, because I think that this can indeed be used in real-life applications."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 230,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2019-10-11T20:03:00.713235Z",
     "start_time": "2019-10-11T20:03:00.696932Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "array([0.01165623031556606, 0.03168492019176483, 0.08612854033708572, 0.234121635556221, 0.6364086270332336], dtype=float)\n",
      "0.2545634508132935\n",
      "array([0.0, 0.0, 0.0, 0.0, 2.0], dtype=float)\n",
      "0.3482121050357819\n",
      "array([0.0, 0.0, 0.0, 2.0, 2.0], dtype=float)\n",
      "0.3826635211706161\n",
      "array([0.0, 0.0, 2.0, 2.0, 2.0], dtype=float)\n",
      "0.3953374892473221\n",
      "array([0.0, 2.0, 2.0, 2.0, 2.0], dtype=float)\n",
      "0.3999999813735485\n",
      "array([2.0, 2.0, 2.0, 2.0, 2.0], dtype=float)\n",
      "0.3999999813735485\n",
      "array([2.0, 2.0, 2.0, 2.0, 2.0], dtype=float)\n",
      "0.3999999813735485\n",
      "array([2.0, 2.0, 2.0, 2.0, 2.0], dtype=float)\n",
      "0.3999999813735485\n",
      "array([2.0, 2.0, 2.0, 2.0, 2.0], dtype=float)\n",
      "0.3999999813735485\n",
      "array([2.0, 2.0, 2.0, 2.0, 2.0], dtype=float)\n",
      "0.3999999813735485\n",
      "array([2.0, 2.0, 2.0, 2.0, 2.0], dtype=float)\n",
      "\n",
      "\n"
     ]
    }
   ],
   "source": [
    "%%micropython -unix 1\n",
    "\n",
    "from ulab import numpy as np\n",
    "\n",
    "def dummy_adc():\n",
    "    # dummy adc function, so that the results are reproducible\n",
    "    return 2\n",
    "    \n",
    "n = 10\n",
    "# These are the normalised weights; the last entry is the most dominant\n",
    "weight = np.exp([1, 2, 3, 4, 5])\n",
    "weight = weight/np.sum(weight)\n",
    "\n",
    "print(weight)\n",
    "# initial array of samples\n",
    "samples = np.array([0]*n)\n",
    "\n",
    "for i in range(n):\n",
    "    # a new datum is inserted on the right hand side. This simply overwrites whatever was in the last slot\n",
    "    samples[-1] = dummy_adc()\n",
    "    print(np.mean(samples[-5:]*weight))\n",
    "    print(samples[-5:])\n",
    "    # the data are shifted by one position to the left\n",
    "    numerical.np(samples, 1)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## flip\n",
    "\n",
    "`numpy`: https://docs.scipy.org/doc/numpy/reference/generated/numpy.flip.html\n",
    "\n",
    "The `flip` function takes one positional, an `ndarray`, and one keyword argument, `axis = None`, and reverses the order of elements along the given axis. If the keyword argument is `None`, the matrix' entries are flipped along all axes. `flip` returns a new copy of the array."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 275,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2019-10-16T06:35:52.163725Z",
     "start_time": "2019-10-16T06:35:52.149231Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "a: \t array([1.0, 2.0, 3.0, 4.0, 5.0], dtype=float)\n",
      "a flipped:\t array([5.0, 4.0, 3.0, 2.0, 1.0], dtype=float)\n",
      "\n",
      "a flipped horizontally\n",
      " array([[3, 2, 1],\n",
      "\t [6, 5, 4],\n",
      "\t [9, 8, 7]], dtype=uint8)\n",
      "\n",
      "a flipped vertically\n",
      " array([[7, 8, 9],\n",
      "\t [4, 5, 6],\n",
      "\t [1, 2, 3]], dtype=uint8)\n",
      "\n",
      "a flipped horizontally+vertically\n",
      " array([[9, 8, 7],\n",
      "\t [6, 5, 4],\n",
      "\t [3, 2, 1]], dtype=uint8)\n",
      "\n",
      "\n"
     ]
    }
   ],
   "source": [
    "%%micropython -unix 1\n",
    "\n",
    "from ulab import numpy as np\n",
    "\n",
    "a = np.array([1, 2, 3, 4, 5])\n",
    "print(\"a: \\t\", a)\n",
    "print(\"a flipped:\\t\", np.flip(a))\n",
    "\n",
    "a = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]], dtype=np.uint8)\n",
    "print(\"\\na flipped horizontally\\n\", np.flip(a, axis=1))\n",
    "print(\"\\na flipped vertically\\n\", np.flip(a, axis=0))\n",
    "print(\"\\na flipped horizontally+vertically\\n\", np.flip(a))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## diff\n",
    "\n",
    "`numpy`: https://docs.scipy.org/doc/numpy/reference/generated/numpy.diff.html\n",
    "\n",
    "The `diff` function returns the numerical derivative of the forward scheme, or more accurately, the differences of an `ndarray` along a given axis. The order of derivative can be stipulated with the `n` keyword argument, which should be between 0, and 9. Default is 1. If higher order derivatives are required, they can be gotten by repeated calls to the function. The `axis` keyword argument should  be -1 (last axis, in `ulab` equivalent to the second axis, and this also happens to be the default value), 0, or 1. \n",
    "\n",
    "Beyond the output array, the function requires only a couple of bytes of extra RAM for the differentiation stencil. (The stencil is an `int8` array, one byte longer than `n`. This also explains, why the highest order is 9: the coefficients of a ninth-order stencil all fit in signed bytes, while 10 would require `int16`.) Note that as usual in numerical differentiation (and also in `numpy`), the length of the respective axis will be reduced by `n` after the operation. If `n` is larger than, or equal to the length of the axis, an empty array will be returned.\n",
    "\n",
    "**WARNING**: the `diff` function does not implement the `prepend` and `append` keywords that can be found in `numpy`. "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 169,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2019-10-31T11:51:02.854338Z",
     "start_time": "2019-10-31T11:51:02.838000Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "a:\n",
      " array([0, 1, 2, 3, 4, 5, 6, 7, 8], dtype=uint8)\n",
      "\n",
      "first derivative:\n",
      " array([1, 1, 1, 1, 1, 1, 1, 1], dtype=uint8)\n",
      "\n",
      "second derivative:\n",
      " array([0, 0, 0, 0, 0, 0, 0], dtype=uint8)\n",
      "\n",
      "c:\n",
      " array([[1.0, 2.0, 3.0, 4.0],\n",
      "\t [4.0, 3.0, 2.0, 1.0],\n",
      "\t [1.0, 4.0, 9.0, 16.0],\n",
      "\t [0.0, 0.0, 0.0, 0.0]], dtype=float)\n",
      "\n",
      "first derivative, first axis:\n",
      " array([[3.0, 1.0, -1.0, -3.0],\n",
      "\t [-3.0, 1.0, 7.0, 15.0],\n",
      "\t [-1.0, -4.0, -9.0, -16.0]], dtype=float)\n",
      "\n",
      "first derivative, second axis:\n",
      " array([[1.0, 1.0, 1.0],\n",
      "\t [-1.0, -1.0, -1.0],\n",
      "\t [3.0, 5.0, 7.0],\n",
      "\t [0.0, 0.0, 0.0]], dtype=float)\n",
      "\n",
      "\n"
     ]
    }
   ],
   "source": [
    "%%micropython -unix 1\n",
    "\n",
    "from ulab import numpy as np\n",
    "\n",
    "a = np.array(range(9), dtype=np.uint8)\n",
    "print('a:\\n', a)\n",
    "\n",
    "print('\\nfirst derivative:\\n', np.diff(a, n=1))\n",
    "print('\\nsecond derivative:\\n', np.diff(a, n=2))\n",
    "\n",
    "c = np.array([[1, 2, 3, 4], [4, 3, 2, 1], [1, 4, 9, 16], [0, 0, 0, 0]])\n",
    "print('\\nc:\\n', c)\n",
    "print('\\nfirst derivative, first axis:\\n', np.diff(c, axis=0))\n",
    "print('\\nfirst derivative, second axis:\\n', np.diff(c, axis=1))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## median\n",
    "\n",
    "`numpy`: https://docs.scipy.org/doc/numpy/reference/generated/numpy.median.html\n",
    "\n",
    "The function computes the median along the specified axis, and returns the median of the array elements. If the `axis` keyword argument is `None`, the arrays is flattened first. The `dtype` of the results is always float."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2020-11-03T19:54:38.047790Z",
     "start_time": "2020-11-03T19:54:38.029264Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "a:\n",
      " array([[0, 1, 2, 3],\n",
      "       [4, 5, 6, 7],\n",
      "       [8, 9, 10, 11]], dtype=int8)\n",
      "\n",
      "median of the flattened array:  5.5\n",
      "\n",
      "median along the vertical axis:  array([4.0, 5.0, 6.0, 7.0], dtype=float)\n",
      "\n",
      "median along the horizontal axis:  array([1.5, 5.5, 9.5], dtype=float)\n",
      "\n",
      "\n"
     ]
    }
   ],
   "source": [
    "%%micropython -unix 1\n",
    "\n",
    "from ulab import numpy as np\n",
    "\n",
    "a = np.array(range(12), dtype=np.int8).reshape((3, 4))\n",
    "print('a:\\n', a)\n",
    "print('\\nmedian of the flattened array: ', np.median(a))\n",
    "print('\\nmedian along the vertical axis: ', np.median(a, axis=0))\n",
    "print('\\nmedian along the horizontal axis: ', np.median(a, axis=1))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## sort\n",
    "\n",
    "`numpy`: https://docs.scipy.org/doc/numpy/reference/generated/numpy.sort.html\n",
    "\n",
    "The sort function takes an ndarray, and sorts its elements in ascending order along the specified axis using a heap sort algorithm. As opposed to the `.sort()` method discussed earlier, this function creates a copy of its input before sorting, and at the end, returns this copy. Sorting takes place in place, without auxiliary storage. The `axis` keyword argument takes on the possible values of -1 (the last axis, in `ulab` equivalent to the second axis, and this also happens to be the default value), 0, 1, or `None`. The first three cases are identical to those in [diff](#diff), while the last one flattens the array before sorting. \n",
    "\n",
    "If descending order is required, the result can simply be `flip`ped, see [flip](#flip).\n",
    "\n",
    "**WARNING:** `numpy` defines the `kind`, and `order` keyword arguments that are not implemented here. The function in `ulab` always uses heap sort, and since `ulab` does not have the concept of data fields, the `order` keyword argument would have no meaning."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 29,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2019-11-05T16:06:27.536193Z",
     "start_time": "2019-11-05T16:06:27.521792Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "\n",
      "a:\n",
      " array([[1.0, 12.0, 3.0, 0.0],\n",
      "\t [5.0, 3.0, 4.0, 1.0],\n",
      "\t [9.0, 11.0, 1.0, 8.0],\n",
      "\t [7.0, 10.0, 0.0, 1.0]], dtype=float)\n",
      "\n",
      "a sorted along vertical axis:\n",
      " array([[1.0, 3.0, 0.0, 0.0],\n",
      "\t [5.0, 10.0, 1.0, 1.0],\n",
      "\t [7.0, 11.0, 3.0, 1.0],\n",
      "\t [9.0, 12.0, 4.0, 8.0]], dtype=float)\n",
      "\n",
      "a sorted along horizontal axis:\n",
      " array([[0.0, 1.0, 3.0, 12.0],\n",
      "\t [1.0, 3.0, 4.0, 5.0],\n",
      "\t [1.0, 8.0, 9.0, 11.0],\n",
      "\t [0.0, 1.0, 7.0, 10.0]], dtype=float)\n",
      "\n",
      "flattened a sorted:\n",
      " array([0.0, 0.0, 1.0, ..., 10.0, 11.0, 12.0], dtype=float)\n",
      "\n",
      "\n"
     ]
    }
   ],
   "source": [
    "%%micropython -unix 1\n",
    "\n",
    "from ulab import numpy as np\n",
    "\n",
    "a = np.array([[1, 12, 3, 0], [5, 3, 4, 1], [9, 11, 1, 8], [7, 10, 0, 1]], dtype=np.float)\n",
    "print('\\na:\\n', a)\n",
    "b = np.sort(a, axis=0)\n",
    "print('\\na sorted along vertical axis:\\n', b)\n",
    "\n",
    "c = np.sort(a, axis=1)\n",
    "print('\\na sorted along horizontal axis:\\n', c)\n",
    "\n",
    "c = np.sort(a, axis=None)\n",
    "print('\\nflattened a sorted:\\n', c)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Heap sort requires $\\sim N\\log N$ operations, and notably, the worst case costs only 20% more time than the average. In order to get an order-of-magnitude estimate, we will take the sine of 1000 uniformly spaced numbers between 0, and two pi, and sort them:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "%%micropython -pyboard 1\n",
    "\n",
    "from ulab import numpy as np\n",
    "\n",
    "@timeit\n",
    "def sort_time(array):\n",
    "    return np.sort(array)\n",
    "\n",
    "b = np.sin(np.linspace(0, 6.28, num=1000))\n",
    "print('b: ', b)\n",
    "sort_time(b)\n",
    "print('\\nb sorted:\\n', b)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## argsort\n",
    "\n",
    "`numpy`: https://docs.scipy.org/doc/numpy/reference/generated/numpy.argsort.html\n",
    "\n",
    "Similarly to [sort](#sort), `argsort` takes a positional, and a keyword argument, and returns an unsigned short index array of type `ndarray` with the same dimensions as the input, or, if `axis=None`, as a row vector with length equal to the number of elements in the input (i.e., the flattened array). The indices in the output sort the input in ascending order. The routine in `argsort` is the same as in `sort`, therefore, the comments on computational expenses (time and RAM) also apply. In particular, since no copy of the original data is required, virtually no RAM beyond the output array is used. \n",
    "\n",
    "Since the underlying container of the output array is of type `uint16_t`, neither of the output dimensions should be larger than 65535. If that happens to be the case, the function will bail out with a `ValueError`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 30,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2019-11-06T06:28:45.719578Z",
     "start_time": "2019-11-06T06:28:45.704072Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "\n",
      "a:\n",
      " array([[1.0, 12.0, 3.0, 0.0],\n",
      "\t [5.0, 3.0, 4.0, 1.0],\n",
      "\t [9.0, 11.0, 1.0, 8.0],\n",
      "\t [7.0, 10.0, 0.0, 1.0]], dtype=float)\n",
      "\n",
      "a sorted along vertical axis:\n",
      " array([[0, 1, 3, 0],\n",
      "\t [1, 3, 2, 1],\n",
      "\t [3, 2, 0, 3],\n",
      "\t [2, 0, 1, 2]], dtype=uint16)\n",
      "\n",
      "a sorted along horizontal axis:\n",
      " array([[3, 0, 2, 1],\n",
      "\t [3, 1, 2, 0],\n",
      "\t [2, 3, 0, 1],\n",
      "\t [2, 3, 0, 1]], dtype=uint16)\n",
      "\n",
      "flattened a sorted:\n",
      " array([3, 14, 0, ..., 13, 9, 1], dtype=uint16)\n",
      "\n",
      "\n"
     ]
    }
   ],
   "source": [
    "%%micropython -unix 1\n",
    "\n",
    "from ulab import numpy as np\n",
    "\n",
    "a = np.array([[1, 12, 3, 0], [5, 3, 4, 1], [9, 11, 1, 8], [7, 10, 0, 1]], dtype=np.float)\n",
    "print('\\na:\\n', a)\n",
    "b = np.argsort(a, axis=0)\n",
    "print('\\na sorted along vertical axis:\\n', b)\n",
    "\n",
    "c = np.argsort(a, axis=1)\n",
    "print('\\na sorted along horizontal axis:\\n', c)\n",
    "\n",
    "c = np.argsort(a, axis=None)\n",
    "print('\\nflattened a sorted:\\n', c)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Since during the sorting, only the indices are shuffled, `argsort` does not modify the input array, as one can verify this by the following example:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 33,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2019-11-06T16:04:31.653444Z",
     "start_time": "2019-11-06T16:04:31.634995Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "\n",
      "a:\n",
      " array([0, 5, 1, 3, 2, 4], dtype=uint8)\n",
      "\n",
      "sorting indices:\n",
      " array([0, 2, 4, 3, 5, 1], dtype=uint16)\n",
      "\n",
      "the original array:\n",
      " array([0, 5, 1, 3, 2, 4], dtype=uint8)\n",
      "\n",
      "\n"
     ]
    }
   ],
   "source": [
    "%%micropython -unix 1\n",
    "\n",
    "from ulab import numpy as np\n",
    "\n",
    "a = np.array([0, 5, 1, 3, 2, 4], dtype=np.uint8)\n",
    "print('\\na:\\n', a)\n",
    "b = np.argsort(a, axis=1)\n",
    "print('\\nsorting indices:\\n', b)\n",
    "print('\\nthe original array:\\n', a)"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "base",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.11.7"
  },
  "toc": {
   "base_numbering": 1,
   "nav_menu": {},
   "number_sections": true,
   "sideBar": true,
   "skip_h1_title": false,
   "title_cell": "Table of Contents",
   "title_sidebar": "Contents",
   "toc_cell": false,
   "toc_position": {
    "height": "calc(100% - 180px)",
    "left": "10px",
    "top": "150px",
    "width": "382.797px"
   },
   "toc_section_display": true,
   "toc_window_display": true
  },
  "varInspector": {
   "cols": {
    "lenName": 16,
    "lenType": 16,
    "lenVar": 40
   },
   "kernels_config": {
    "python": {
     "delete_cmd_postfix": "",
     "delete_cmd_prefix": "del ",
     "library": "var_list.py",
     "varRefreshCmd": "print(var_dic_list())"
    },
    "r": {
     "delete_cmd_postfix": ") ",
     "delete_cmd_prefix": "rm(",
     "library": "var_list.r",
     "varRefreshCmd": "cat(var_dic_list()) "
    }
   },
   "types_to_exclude": [
    "module",
    "function",
    "builtin_function_or_method",
    "instance",
    "_Feature"
   ],
   "window_display": false
  }
 },
 "nbformat": 4,
 "nbformat_minor": 4
}
